﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Timers;

namespace ScriptEase
{


    /// <summary>
    /// The FileSystemwatcher2 basically works as the <see cref="FileSystemWatcher"/>. 
    /// The FileSystemwatcher2 suspresses the FileSystemWatcher's event 'jittering' 
    /// by delaying an event for a specified time (<see cref="_delay"/>).
    /// </summary>
    /// <remarks>
    /// FileSystemWatcher notifications are put into an <see cref="_eventQueue">event queue</see> 
    /// and each event will stay there for the <see cref="_delay">configured delay time</see>. 
    /// Subsequent events of the same type will charge the timer of the queue item so that
    /// the <see cref="FileChangedDelayed">notification of the file</see> change will be raised 
    /// n milliseconds after the last event of the same type. 
    /// </remarks>
    class FileSystemwatcher2 : IDisposable
    {
        public FileSystemwatcher2(string fullPath)
        {
            _eventQueue = new Dictionary<string, QueueItem>(StringComparer.OrdinalIgnoreCase);
            _delay = 500;
            string directoryName = Path.GetDirectoryName(fullPath);
            string fileName = Path.GetFileName(fullPath);

            _fileSystemWatcher = new FileSystemWatcher(directoryName, fileName);
            _fileSystemWatcher.Changed += new FileSystemEventHandler(fileChanged);
            _fileSystemWatcher.EnableRaisingEvents = true;
            //_fileSystemWatcher.Filter = "*.*";
            //_fileSystemWatcher.IncludeSubdirectories = true;
            _fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.CreationTime;

            _queueTimer = new Timer();
            _queueTimer.Elapsed += new ElapsedEventHandler(queueTimerElapsed);
            _queueTimer.Interval = 125;
            _queueTimer.AutoReset = true;
            _queueTimer.Start();
        }


        public event FileSystemEventHandler FileChangedDelayed;



        private class QueueItem
        {
            public QueueItem(DateTime fired, FileSystemEventArgs eventArgs)
            {
                Fired = fired;
                EventArgs = eventArgs;
            }



            public DateTime Fired { get; private set; }
            public FileSystemEventArgs EventArgs { get; private set; }
        }



        /// <summary>
        /// Gets or sets the delay during which events of the same type are ignored.
        /// </summary>
        private readonly int _delay;


        private readonly Dictionary<string, QueueItem> _eventQueue;
        private readonly Timer _queueTimer;
        private static readonly object _queueLock = new object();
        private readonly FileSystemWatcher _fileSystemWatcher;



        /// <summary>
        /// Teh FileSystemWatcher has recognized a file change.
        /// </summary>
        /// <remarks>
        /// Put the event information into the 
        /// <see cref="_eventQueue">event queue</see> and charge the timer
        /// </remarks>
        private void fileChanged(object sender, FileSystemEventArgs e)
        {
            lock (_queueLock)
            {

                DateTime fireTime = DateTime.Now;
                Trace.Default.Debug("NewItem/Update queue item {0}; Time={1}", e.Name, fireTime);
                _eventQueue[e.FullPath] = new QueueItem(fireTime, e);
            }
        }



        /// <summary>
        /// The queue the timer elapsed.
        /// </summary>
        /// <remarks>
        /// Poll all items in the _eventQueue and check which timers have 
        /// elapsed, compared to the delay time. Then raise the 
        /// <see cref="FileChangedDelayed"/> event for each item and remove 
        /// the item from the queue.<br />
        /// </remarks>
        private void queueTimerElapsed(object sender, ElapsedEventArgs e)
        {
            lock (_queueLock)
            {
                DateTime reference = DateTime.Now;
                //Trace.Default.Debug("Timer reference time {0}. {1} item in the queue at all", reference, _eventQueue.Count);

                QueueItem[] readyItems
                    = _eventQueue.Values.Where(qi => reference.Subtract(qi.Fired).TotalMilliseconds > this._delay).ToArray();

                foreach (var readyItem in readyItems)
                {
                    onChangeDelayed(readyItem);
                    _eventQueue.Remove(readyItem.EventArgs.FullPath);
                    Trace.Default.Debug("QueueItem {0}", _eventQueue.Count());
                }
            }
        }



        /// <summary>
        /// Fire the FileChangedDelayed event.
        /// </summary>
        /// <param name="queueItem">The event item.</param>
        private void onChangeDelayed(QueueItem queueItem)
        {
            if (this.FileChangedDelayed == null) return;

            Trace.Default.Debug("Raise change delay event {0}", queueItem.EventArgs.Name);
            this.FileChangedDelayed.BeginInvoke(this, queueItem.EventArgs, null, null);
        }



        private void disposeManaged()
        {
            if (_queueTimer != null)
            {
                _queueTimer.Stop();
                _queueTimer.Dispose();
            }

            if (_fileSystemWatcher != null)
            {
                _fileSystemWatcher.Changed -= new FileSystemEventHandler(fileChanged);
                _fileSystemWatcher.Dispose();
            }
        }



        #region IDisposable


        public void Dispose()
        {
            dispose(true);
            GC.SuppressFinalize(this);
        }



        private void dispose(bool disposing)
        {
            if (!this._disposed)
            {
                if (disposing)
                    disposeManaged();
                //CloseHandle(handle);
            }
            _disposed = true;
        }



        private bool _disposed;


        #endregion
    }
}